/** * (c) Copyright 2007-2010 by emarsys eMarketing Systems AG * * This file is part of dyson. * * dyson is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * dyson is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.emarsys.dyson; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Calendar; import java.util.Collection; import java.util.LinkedList; import java.util.Map; import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; import javax.mail.Session; import javax.mail.Transport; import org.restlet.Component; import org.restlet.data.Protocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.subethamail.smtp.MessageListener; import org.subethamail.smtp.server.SMTPServer; import com.emarsys.ecommon.builder.time.CalendarBuilder; import com.emarsys.ecommon.collections.MapUtil; import com.emarsys.ecommon.concurrent.Threads; import com.emarsys.ecommon.mail.JMailProperties; import com.emarsys.ecommon.prefs.config.Configuration; import com.emarsys.ecommon.prefs.config.ConfigurationBackend; import com.emarsys.ecommon.prefs.config.ConfigurationDeclaration; import com.emarsys.ecommon.prefs.config.ISetting; import com.emarsys.ecommon.prefs.config.backend.DefaultsConfigurationBackend; import com.emarsys.ecommon.prefs.config.backend.PropertiesConfigurationBackend; import com.emarsys.ecommon.time.Dates; import com.emarsys.ecommon.util.Classes; import com.emarsys.ecommon.util.StopableRunnable; /** * <p> * {@link DysonServer} is the default implementation of the {@link Dyson} MTA. * </p><p> * See {@link Dyson} for further details. * </p> * * @author <a href="mailto:kulovits@emarsys.com">Michael "kULO" Kulovits</a> */ public class DysonServer implements Dyson, DysonStatistics, StopableRunnable { private static final Logger log = LoggerFactory.getLogger( DysonServer.class ); //dyson sever fields /** * flag that indicates shutdown requests to dyson */ protected volatile boolean shouldStop = false; /** * dyson's {@link Configuration}. */ protected Configuration config; /** * dyson's {@link SMTPServer} component. */ protected SMTPServer smtpServer; /** * dyson's {@link DysonStorage storage}. */ protected DysonStorage storage; /** * the {@link MessageListener}s */ protected Collection<MessageListener> msgListeners; /** * The RESTlet {@link Component} exposed by dyson. */ protected Component restComponent; //runtime information protected Calendar startTime = null; protected AtomicInteger nbrOfHandledMails = new AtomicInteger( 0 ); protected AtomicInteger nbrOfIncomingMails = new AtomicInteger( 0 ); protected AtomicInteger nbrOfDiscardedMails = new AtomicInteger( 0 ); protected AtomicInteger nbrOfProcessedMails = new AtomicInteger( 0 ); //javamail "proxy" to dyson /** * a JavaMail(tm) session that has this * {@link DysonServer} configured as default {@link Transport}. */ protected Session session; /** * <p> * Creates a new {@link DysonServer} instance. * </p><p> * Initializes the server's {@link #initConfig(String[]) config}, * {@link #initStorage() storage}, {@link #initMsgListeners() * message listeners} as well as the {@link #initSmtp() SMTP server}. * </p> */ public DysonServer() { this.initConfig(); this.initStorage(); this.initMsgListeners(); this.initSmtp(); this.initRestServer(); this.registerJvmShutdownHook(); } /** * @see Dyson#newDysonPart(String, Class) */ public <P extends DysonPart> P newDysonPart( String settingName, Class<? extends P> partType ) { if( settingName == null || settingName.length() == 0 ) { throw new IllegalArgumentException( "invalid setting name for specifying the " + "concrete dyson part implementation: " + settingName ); } if( partType == null ) { throw new IllegalArgumentException( "dyson part type is null: " + settingName ); } String partClassName = this.getConfiguration().get( settingName ).getValue(); P part = Classes.instantiator( partType, DysonException.class ) .forName( partClassName ) .useConstructor( Dyson.class ) .withParams( this ) .newInstance(); return part; } /** * <p> * Initializes the server's {@link #config configuration}. * </p><p> * Creates the {@link Configuration} for this server by wrapping * the {@link System#getProperties() system properties} into a * {@link PropertiesConfigurationBackend} that uses <tt>this</tt> * {@link Dyson} instance (i.e. the inherited {@link DysonConfig}) * as the source for its {@link ConfigurationDeclaration}. * </p> */ protected void initConfig() { ConfigurationBackend backend, sysPropsBackend, filePropsBackend = null; sysPropsBackend = PropertiesConfigurationBackend.wrap( System.getProperties() ); Properties propsFromFile = this.loadPropsFromFile(); if( propsFromFile != null && !propsFromFile.isEmpty() ) { filePropsBackend = PropertiesConfigurationBackend.wrap( propsFromFile ); backend = DefaultsConfigurationBackend.cascade( filePropsBackend, sysPropsBackend ); } else { backend = sysPropsBackend; } this.config = Configuration.getInstanceDeclaredBy( backend, this ); this.logConfig(); } /** * Writes the initialized settings/config as debug messages * to the log file. */ protected void logConfig() { log.debug( "initializing config..."); ISetting setting; for( String name: config.getDeclaration().getSettingNames() ) { setting = config.get( name ); log.debug( "got setting: {}={}", name, (setting == null ? null : setting.getValue()) ); } } /** * Loads the {@link Properties} from * {@link DysonConfig#SERVER_PROPERTIES_FILE server props file}. * * @return the loaded {@link Properties} or <code>null</code> * if no properties file has been specified or found. */ protected Properties loadPropsFromFile() { Properties result = null; String propsFileName = System.getProperty( SERVER_PROPERTIES_FILE ); if( propsFileName != null ) { try { result = new Properties(); result.load( new FileInputStream( propsFileName ) ); } catch( FileNotFoundException fnfe ) { log.warn( "cannot load properties from " + "non-exitent file \'{}\': {}", propsFileName, fnfe ); } catch (IOException ioe) { log.warn( "cannot load properties from " + "file \'{}\': {}", propsFileName, ioe ); } } return result; } /** * <p> * {@link DysonStorage#initialize(Configuration) initializes} * the {@link DysonStorage storage} with this server's * {@link #config configuration}. * </p><p> * The configuration must have been initialized already! * </p> */ protected void initStorage() { assert this.config != null; this.storage = DysonStorage.getInstance( this ); } /** * <p> * Initializes the {@link #msgListeners message listeners} for * the {@link #smtpServer SMTP server}. * </p><p> * A new {@link #newStorageMessageListener() storage message listener} * will be preset for the registration with the {@link #getSmtpServer() * SMTP server} * </p> * @return */ protected void initMsgListeners() { this.msgListeners = new LinkedList<MessageListener>(); this.msgListeners.add( this.newStorageMessageListener() ); log.debug( "initialized message listeners: {}", this.msgListeners ); } /** * factory method for the {@link MessageListener message listeners} * which have to be registered with the {@link SMTPServer SMTP component}. * * @return * @throws DysonException */ protected MessageListener newStorageMessageListener() throws DysonException { return this.newDysonPart( DysonConfig.STORAGE_INCOMING_MESSAGE_LISTENER_CLASS, DysonMessageListener.class ); } /** * Initializes dyson's {@link #smtpServer SMTP server} with its * {@link #config configuration}. */ protected void initSmtp() { assert this.config != null; assert this.msgListeners != null; this.smtpServer = new SMTPServer( this.msgListeners ); this.smtpServer.setPort( this.config.get( SMTP_PORT ).getIntValue() ); this.smtpServer.setConnectionTimeout( this.config.get( SMTP_CONNECTION_TIMEOUT_MILLIS ).getIntValue() ); this.smtpServer.setDataDeferredSize( this.config.get( SMTP_MAIL_DISK_CACHING_THRESHOLD_BYTES ).getIntValue() ); this.smtpServer.setMaxConnections( this.config.get( SMTP_MAX_CONNECTIONS ).getIntValue() ); this.smtpServer.setAnnounceTLS( this.config.get( SMTP_ANNOUNCE_TLS_SUPPORT ).getBooleanValue() ); log.debug( "initialized SMTP server" ); } /** * initializes dyson's REST server instance with port specified * under {@link DysonConfig#REST_SERVER_PORT} and registers as new * instance of {@link DysonConfig#REST_SERVER_ROOT_RESTLET_CLASS}. */ protected void initRestServer() { assert this.config != null; int port = this.getConfiguration().get( REST_SERVER_PORT ).getIntValue(); DysonRestApp app = this.newDysonPart( REST_APPLICATION_CLASS, DysonRestApp.class ); // this.restServer = new Server( Protocol.HTTP, port, app ); Component component = new Component(); component.getServers().add( Protocol.HTTP, port ); component.getClients().add( Protocol.FILE ); component.getDefaultHost().attach( app ); this.restComponent = component; } /** * register server shutdown hook on jvm shutdown if enabled in the config */ protected void registerJvmShutdownHook() { if( this.config.get( SERVER_SHUTDOWN_HOOK_ENABLED ).getBooleanValue() ) { Runnable finalizer = new Runnable() { public void run() { if( isRunning() ) { log.debug( "executing dyson's JVM shutdown hook..." ); stop(); } } }; Runtime.getRuntime().addShutdownHook( new Thread( finalizer ) ); } } /** * @see com.emarsys.dyson.Dyson#getConfiguration() */ public Configuration getConfiguration() { return this.config; } /** * @see Dyson#getSmtpServer() */ public SMTPServer getSmtpServer() { return smtpServer; } /** * @see Dyson#getStorage() */ public DysonStorage getStorage() { return this.storage; } /** * @see Dyson#getRestComponent() */ public Component getRestComponent() { return this.restComponent; } /** * Sets the {@link Properties} needed to * configure this dyson instance as the sessions's * {@link Transport}. * * @param props */ protected Properties getJavaMailSmtpProps() { JMailProperties props = JMailProperties.getInstance( System.getProperties() ); props.putAll( MapUtil.getMap( JMailProperties.MAIL_SMTP_HOST, this.smtpServer.getHostName(), JMailProperties.MAIL_SMTP_PORT, String.valueOf( this.smtpServer.getPort() ) ) ); return props; } /** * @see com.emarsys.dyson.Dyson#getStatistics() */ public DysonStatistics getStatistics() { return this; } /** * @see com.emarsys.dyson.Dyson#getJMailSession() */ public Session getJMailSession() { if( this.session == null ) { this.session = Session.getInstance( this.getJavaMailSmtpProps() ); } return this.session; } /** * @see com.emarsys.dyson.Dyson#isRunning() */ public synchronized boolean isRunning() { return this.smtpServer.isRunning() || this.storage.isRunning() || this.restComponent.isStarted(); } /** * Starts the dyson server's SMTP and storage components * asynchronously. * * @see org.subethamail.smtp.server.SMTPServer#start() * @see DysonStorage#start() */ public synchronized void start() throws DysonException { try { this.startTime = CalendarBuilder.getInstance(); log.info( "starting dyson server..." ); this.storage.start(); log.info( "starting dyson SMTP component..." ); this.smtpServer.start(); log.info( "starting REST component..." ); this.restComponent.start(); } catch( Exception ex ) { throw new DysonException( "cannot start dyson server:" + ex, ex ); } } /** * <p> * Stops the dyson server's SMTP and storage components * asynchronously. * </p><p> * This methods will not wait (block) for the components * to be stopped. * </p><p> * It is assured that * </p> * * @see org.subethamail.smtp.server.SMTPServer#stop() * @see DysonStorage#stop() */ public synchronized void stop() { Runnable smtpStopper, storageStopper, restStopper; smtpStopper = new Runnable() { public void run() { log.info( "stopping dyson SMTP component..." ); DysonServer.this.smtpServer.stop(); } }; restStopper = new Runnable() { public void run() { log.info( "stopping dyson REST component" ); try { DysonServer.this.restComponent.stop(); } catch( Exception ex ) { throw new DysonException( "error on stopping dyson REST component: " + ex, ex ); } } }; storageStopper = new Runnable() { public void run() { DysonServer.this.storage.stop(); } }; log.info( "stopping dyson server..." ); this.shouldStop = true; Threads.runAsynchronouslyIgnoringRTEs( smtpStopper ); Threads.runAsynchronouslyIgnoringRTEs( restStopper ); Threads.runAsynchronouslyIgnoringRTEs( storageStopper ); } /** * @see java.lang.Runnable#run() */ public void run() { this.start(); //TODO ugly busy waiting while( !this.shouldStop ) { Threads.sleepSilently( Dates.SECOND_IN_MILLIS ); } } /** * @see com.emarsys.dyson.Dyson#getRuntimeInformation() */ public Map<String,String> getRuntimeInformation() { return MapUtil.getSortedMap( "start.time", Dates.timestampToString( this.startTime ), "mail.handled.count", this.nbrOfHandledMails.toString(), "mail.discarded.count", this.nbrOfDiscardedMails.toString(), "mail.incoming.count", this.nbrOfIncomingMails.toString(), "mail.processed.count", this.nbrOfProcessedMails.toString() ); } /** * @see com.emarsys.dyson.DysonStatistics#fire(com.emarsys.dyson.DysonStatistics.MailEvent) */ public void fire( MailEvent event ) { int cnt; //TODO replace with more dynamic event handling //this code has to be changed on every change to MailEvent(s) => ugly //introduce a registry for listeners in DysonStatistics switch( event ) { case MAIL_HANDLED: cnt = this.nbrOfHandledMails.getAndIncrement(); log.debug( "handled mail #{}", cnt ); break; case MAIL_DISCARDED: cnt = this.nbrOfDiscardedMails.getAndIncrement(); log.debug( "discarded mail #{}", cnt ); break; case MAIL_CAME_IN: cnt = this.nbrOfIncomingMails.getAndIncrement(); log.debug( "got incoming mail #{}", cnt ); break; case MAIL_PROCESSED: cnt = this.nbrOfProcessedMails.getAndIncrement(); log.debug( "processed mail #{}", cnt ); break; default: log.warn( "ignoring unknown event: " + event ); } } /** * <p> * Initializes a {@link DysonServer} {@link #start()}s it. * </p><p> * {@link #DysonServer() Creates} a new server instance * and {@link #run() runs} it. Every uncaught exception will * be logged and finally the server will be {@link #stop() stopped}. * </p><p> * The dyson server can be configured using {@link System#getProperties() * system properties } or a seperate {@link DysonConfig#SERVER_PROPERTIES_FILE * properties file}, see {@link DysonConfig} for further details. * </p> * * @param args - the cmd line arguments which are ignored * * @see #DysonServer(String[]) * @see #run() */ public static void main( String[] args ) { DysonServer server = null; try { server = new DysonServer(); server.run(); } catch( Throwable th ) { log.error( "fatal error in dyson server: " + th, th ); } finally { if( server != null && server.isRunning() ) { server.stop(); } } } }//class DysonServer